/*
 * @(#)Handler.java	1.26 02/08/21
 *
 * Copyright (c) 1996-2002 Sun Microsystems, Inc.  All rights reserved.
 */

package com.sun.media.datasink.file;

import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import javax.media.Control;
import javax.media.IncompatibleSourceException;
import javax.media.MediaLocator;
import javax.media.protocol.DataSource;
import javax.media.protocol.PullDataSource;
import javax.media.protocol.PushDataSource;
import javax.media.protocol.PushSourceStream;
import javax.media.protocol.Seekable;
import javax.media.protocol.SourceStream;
import javax.media.protocol.SourceTransferHandler;

import com.ms.security.PermissionID;
import com.ms.security.PolicyEngine;
import com.sun.media.JMFSecurity;
import com.sun.media.JMFSecurityManager;
import com.sun.media.Syncable;
import com.sun.media.datasink.BasicDataSink;
import com.sun.media.datasink.RandomAccess;
import com.sun.media.util.jdk12;
import com.sun.media.util.jdk12DeleteFileAction;

public class Handler
    extends BasicDataSink
    implements SourceTransferHandler, Seekable, Runnable, RandomAccess, Syncable {

    final private static   boolean DEBUG = false;
    
    final protected static int NOT_INITIALIZED = 0;
    final protected static int OPENED = 1;
    final protected static int STARTED = 2;
    //final protected static int STOPPED = 3;
    final protected static int CLOSED = 3;
    
    protected int state = NOT_INITIALIZED;

    protected DataSource source;
    protected SourceStream [] streams;
    protected SourceStream stream;
    protected boolean push;

    protected boolean errorEncountered = false;
    protected String  errorReason = null;

    protected Control [] controls;
    
    protected File file;
    protected File tempFile = null;
    protected RandomAccessFile raFile = null;
    protected RandomAccessFile qtStrRaFile = null;
    protected boolean          fileClosed = false;
    protected FileDescriptor fileDescriptor = null;
    protected MediaLocator locator = null;
    protected String contentType = null;
    protected int fileSize = 0; // Good for over 2 Gigabytes
    protected int filePointer = 0;
    protected int bytesWritten = 0;
    protected static final int BUFFER_LEN = 128 * 1024;
    protected boolean syncEnabled = false;

    protected byte [] buffer1 = new byte[BUFFER_LEN];
    protected byte [] buffer2 = new byte[BUFFER_LEN];
    protected boolean buffer1Pending = false;
    protected long    buffer1PendingLocation = -1;
    protected int     buffer1Length;
    protected boolean buffer2Pending = false;
    protected long    buffer2PendingLocation = -1;
    protected int     buffer2Length;
    protected long    nextLocation = 0;
    protected Thread  writeThread = null;
    private   Integer bufferLock = new Integer(0);
    private   boolean receivedEOS = false;

    private static JMFSecurity jmfSecurity = null;
    private static boolean securityPrivelege=false;
    private Method m[] = new Method[1];
    private Class cl[] = new Class[1];
    private Object args[][] = new Object[1][0];
    public  int WRITE_CHUNK_SIZE = 16384;

    private boolean streamingEnabled = false;
    private boolean errorCreatingStreamingFile = false;
    static {
	try {
	    jmfSecurity = JMFSecurityManager.getJMFSecurity();
	    securityPrivelege = true;
	} catch (SecurityException e) {
	}
    }
    
    public void setSource(DataSource ds)
	throws IncompatibleSourceException {
	
	if (!(ds instanceof PushDataSource) &&
	    !(ds instanceof PullDataSource)) {

	    throw new IncompatibleSourceException("Incompatible datasource");
	}
	source = ds;
	
	if (source instanceof PushDataSource) {
	    push = true;
	    try {
		((PushDataSource) source).connect();
	    } catch (IOException ioe) {
	    }
	    streams = ((PushDataSource) source).getStreams();
	} else {
	    push = false;
	    try {
		((PullDataSource) source).connect();
	    } catch (IOException ioe) {
	    }
	    streams = ((PullDataSource) source).getStreams();
	}
	
	if (streams == null || streams.length != 1)
	    throw new IncompatibleSourceException("DataSource should have 1 stream");
	stream = streams[0];
	
	contentType = source.getContentType();
	if (push)
	    ((PushSourceStream)stream).setTransferHandler(this);
    }

    /**
     * Set the output <code>MediaLocator</code>.
     * This method should only be called once; an error is thrown if
     * the locator has already been set.
     * @param output <code>MediaLocator</code> that describes where 
     * 		the output goes.
     */
    public void setOutputLocator(MediaLocator output) {
	locator = output;
    }

    public void setEnabled(boolean b) {
	streamingEnabled = b;
    }

    public void setSyncEnabled() {
	syncEnabled = true;
    }

    // write chunk
    // Call with -1, -1 when done writing chunks to streamable file
    // Call with -1, (num > 0) to seek to num-1 and write 1 byte
    public boolean write(long inOffset, int numBytes) {
	try {
	    if ( (inOffset >= 0) && (numBytes > 0 ) ) {
		int remaining = numBytes;
		int bytesToRead;
		raFile.seek(inOffset);
		
		while (remaining > 0) {
		    bytesToRead = (remaining > BUFFER_LEN) ? BUFFER_LEN : remaining;
		    raFile.read(buffer1, 0, bytesToRead); //$$ CAST
		    qtStrRaFile.write(buffer1, 0, bytesToRead); //$$ CAST
		    remaining -= bytesToRead;
		}
	    } else if ( (inOffset < 0) && (numBytes > 0) ) {
		qtStrRaFile.seek(0);
		qtStrRaFile.seek(numBytes-1);
		qtStrRaFile.writeByte(0);
		qtStrRaFile.seek(0);
	    } else {
		sendEndofStreamEvent();
	    }
	} catch (Exception e) {
	    errorCreatingStreamingFile = true;
	    System.err.println("Exception when creating streamable version of media file: " +
			       e.getMessage());
	    return false;
	}
	return true;
    }


    public void open() throws IOException, SecurityException {
	try {
	    if ( state == NOT_INITIALIZED ) {

		if (locator != null) {
		    String pathName = locator.getRemainder(); // getFileName(locator);
		    // Strip off excess /'s 
		    while (pathName.charAt(0) == '/' &&
			   (pathName.charAt(1) == '/' || pathName.charAt(2) == ':')) {
			pathName = pathName.substring(1);
		    }
		    String fileSeparator = System.getProperty("file.separator");
		    if (fileSeparator.equals("\\")) {
			pathName = pathName.replace('/', '\\');
		    }
		
		    com.sun.media.JMFSecurityManager.checkFileSave();
		    if ( securityPrivelege && (jmfSecurity != null) ) {
			String permission = null;
			try {

			    if (jmfSecurity.getName().startsWith("jmf-security")) {
				permission = "read file";
				jmfSecurity.requestPermission(m, cl, args, JMFSecurity.READ_FILE);
				m[0].invoke(cl[0], args[0]);
			
				permission = "write file";
				jmfSecurity.requestPermission(m, cl, args, JMFSecurity.WRITE_FILE);
				m[0].invoke(cl[0], args[0]);
			    } else if (jmfSecurity.getName().startsWith("internet")) {
				PolicyEngine.checkPermission(PermissionID.FILEIO);
				PolicyEngine.assertPermission(PermissionID.FILEIO);
			    }
			} catch (Exception e) {
			    securityPrivelege = false;
			    if (push)
				((PushSourceStream)stream).setTransferHandler(null);
			    throw new SecurityException(e.getMessage());
			} catch (Error e) {
			    securityPrivelege = false;
			    if (push)
				((PushSourceStream)stream).setTransferHandler(null);
			    throw new SecurityException(e.getMessage());
			}
		    }
		    if (!securityPrivelege) {
			if (push)
			    ((PushSourceStream)stream).setTransferHandler(null);
			throw new IOException("Datasink: Unable to get security privileges for file writing");
		    }

		    // In jdk1.2, RandomAccessFile has a useful method called
		    // setLength() which can be used to truncate an existing
		    // file. But this won't work on jdk1.1. So we have to
		    // delete a file if it exists

		    // On Windows, you cannot delete a file if some process
		    // is using it.

		    file = new File(pathName);
		    if (file.exists()) {
			if (!deleteFile(file)) {
			    System.err.println("datasink open: Existing file "
					       + pathName +
					       " cannot be deleted. Check if " +
					       "some other process is using " +
					       " this file");
			    if (push)
				((PushSourceStream)stream).setTransferHandler(null);
			    throw new IOException("Existing file " + pathName +
						  " cannot be deleted");
			}
		    }

		    String parent = file.getParent();
		    if (parent != null) {
			new File(parent).mkdirs();
		    }
		    try {
			if (!streamingEnabled) {
			    raFile = new RandomAccessFile(file, "rw");
			    fileDescriptor = raFile.getFD();
			} else {
			    String fileqt;
			    int index;
			    if ( (index = pathName.lastIndexOf(".")) > 0 ) {
				// TODO: maybe $$$ add random string
				fileqt = pathName.substring(0, index) + ".nonstreamable" +
				    pathName.substring(index, pathName.length());
			    } else {
				// TODO: maybe $$$ add random string
				// TODO: Don't assume .mov if the file doesn't have
				// and extension. Try to use content type if possible
				// to guess the extension.
				// However, extensions will be there as JMF provides it
				// even if user doesn't.
				fileqt =  file + ".nonstreamable.mov"; 
			    }
			    tempFile = new File(fileqt);

			    raFile = new RandomAccessFile(tempFile, "rw");
			    fileDescriptor = raFile.getFD();
			    qtStrRaFile = new RandomAccessFile(file, "rw");
			}
		    } catch (IOException e) {
			// Catch the exception for debugging purpose and
			// throw it again
			System.err.println("datasink open: IOException when creating RandomAccessFile "
					   + pathName + " : " + e);
			if (push)
			    ((PushSourceStream)stream).setTransferHandler(null);
			throw e;
		    }

		    setState(OPENED);
		}
	    }
	} finally {
	    if ( (state == NOT_INITIALIZED) && (stream != null) ) {
		((PushSourceStream)stream).setTransferHandler(null);
	    }
	}
    }


    public MediaLocator getOutputLocator() {
	return locator;
    }

    public void start() throws IOException {
	if (state == OPENED) {
	    if (source != null)
		source.start();
	    if (writeThread == null) {
		writeThread = new Thread(this);
		writeThread.start();
	    }
	    setState(STARTED);
	}
    }

    /**
     * Stop the data-transfer.
     * If the source has not been connected and started,
     * <CODE>stop</CODE> does nothing.
     */
    public void stop() throws IOException {
	if (state == STARTED) {
	    if (source != null)
		source.stop();
	    setState(OPENED);
	}
    }

    protected void setState(int state) {
	synchronized(this) {
	    this.state = state;
	}
    }

    public void close() {
	close(null); // No Error;
    }

    protected final void close(String reason) {
	synchronized(this) {
	    if ( state == CLOSED )
		return;
	    setState(CLOSED);
	}

	if (push) {
	    for (int i = 0; i < streams.length; i++) {
		((PushSourceStream)streams[i]).setTransferHandler(null);
	    }
	}

	if (reason != null) {
	    errorEncountered = true;
	    sendDataSinkErrorEvent(reason);
	    // Wake up the write thread
	    synchronized (bufferLock) {
		bufferLock.notifyAll();
	    }
	}

	// Wait for all buffers to be written
	/*
	synchronized (bufferLock) {
	    while (reason == null && (buffer2Pending || buffer1Pending)) {
		try {
		    bufferLock.wait(250);
		} catch (InterruptedException ie) {
		}
	    }
	}
	*/
	
	try {
	    source.stop();
	} catch (IOException e) {
	    System.err.println("IOException when stopping source " + e);
	}
	


	try {
	    if (raFile != null) {
		raFile.close();
	    }

	    if (streamingEnabled) {
		if (qtStrRaFile != null) {
		    qtStrRaFile.close();
		}
	    }

	    // 	Disconnect the data source 
	    if (source != null)
		source.disconnect();
	    ////////////////////////////////////////////////////////////


	    if (streamingEnabled && (tempFile != null) ) {
		// Delete the temp file if no errors creating streamable file.
		// If errors creating streamable file, delete streamable file.
		if (!errorCreatingStreamingFile) {
		    boolean status = deleteFile(tempFile);
		} else {
		    boolean status = deleteFile(file);
		}
	    }
// 	    fileClosed = true;
// 	    sendEndofStreamEvent();
	} catch (IOException e) {
	    System.out.println("close: " + e);
	}
	
	raFile = null; // Should be done after setting the state to CLOSED
	qtStrRaFile = null;

	removeAllListeners();
    }

    public String getContentType() {
	return contentType;
    }

    public Object [] getControls() {
	if (controls == null) {
	    controls = new Control[0];
	}
	return controls;
    }

    public Object getControl(String controlName) {
	return null;
    }

    // TODO : Handle pull data source

    public synchronized void transferData(PushSourceStream pss) {
	int totalRead = 0;
	int spaceAvailable = BUFFER_LEN;
	int bytesRead = 0;
	if (errorEncountered)
	    return;

	if (buffer1Pending) {
	    synchronized (bufferLock) {
		while (buffer1Pending) {
		    if (DEBUG) System.err.println("Waiting for free buffer");
		    try {
			bufferLock.wait();
		    } catch (InterruptedException ie) {
		    }
		}
	    }
	    if (DEBUG) System.err.println("Got free buffer");
	}
	
	//	System.err.println("In transferData()");
	while (spaceAvailable > 0) {
	    try {
		bytesRead = pss.read(buffer1, totalRead, spaceAvailable);
		//System.err.println("bytesRead = " + bytesRead);
		if (bytesRead > 16 * 1024 && WRITE_CHUNK_SIZE < 32 * 1024) {
		    if (  bytesRead > 64 * 1024 &&
			  WRITE_CHUNK_SIZE < 128 * 1024  )
			WRITE_CHUNK_SIZE = 128 * 1024;
		    else if (  bytesRead > 32 * 1024 &&
			       WRITE_CHUNK_SIZE < 64 * 1024  )
			WRITE_CHUNK_SIZE = 64 * 1024;
		    else if (  WRITE_CHUNK_SIZE < 32 * 1024  )
			WRITE_CHUNK_SIZE = 32 * 1024;
		    //System.err.println("Increasing buffer to " + WRITE_CHUNK_SIZE);

		}
	    } catch (IOException ioe) {
		// What to do here?
	    }
	    if (bytesRead <= 0) {
		break;
	    }
	    totalRead += bytesRead;
	    spaceAvailable -= bytesRead;
	}

	if (totalRead > 0) {
	    synchronized (bufferLock) {
		buffer1Pending = true;
		buffer1PendingLocation = nextLocation;
		buffer1Length = totalRead;
		nextLocation = -1; // assume next write is contiguous unless seeked
		// Notify availability to write thread
		if (DEBUG) System.err.println("Notifying consumer");
		bufferLock.notifyAll();
	    }
	}
	// Send EOS if necessary
	if (bytesRead == -1) {
	    if (DEBUG) System.err.println("Got EOS");
	    receivedEOS = true;
	    // Wait until file is closed. This makes the Processor's close
	    // call to force the data sink to close the file, just in case
	    // the user doesn't remember to close the datasink before exiting.
	    while (!fileClosed && !errorEncountered && !(state == CLOSED)) {
		try {
		    Thread.sleep(50);
		} catch (InterruptedException ie) {
		}
	    }
	}
    }

    // Asynchronous write thread
    public void run() {
	while (!(state == CLOSED || errorEncountered)) {
	    synchronized (bufferLock) {
		// Wait for some data or error
		while (!buffer1Pending && !buffer2Pending &&
		       !errorEncountered && state != CLOSED && !receivedEOS) {
		    if (DEBUG) System.err.println("Waiting for filled buffer");
		    try {
			bufferLock.wait(500);
		    } catch (InterruptedException ie) {
		    }
		    if (DEBUG) System.err.println("Consumer notified");
		}
	    }
	    // Something's pending
	    if (buffer2Pending) {
		if (DEBUG) System.err.println("Writing Buffer2");
		// write that first
		write(buffer2, buffer2PendingLocation, buffer2Length);
		if (DEBUG) System.err.println("Done writing Buffer2");
		buffer2Pending = false;
	    }

	    synchronized (bufferLock) {
		if (buffer1Pending) {
		    byte [] tempBuffer = buffer2;
		    buffer2 = buffer1;
		    buffer2Pending = true;
		    buffer2PendingLocation = buffer1PendingLocation;
		    buffer2Length = buffer1Length;
		    buffer1Pending = false;
		    buffer1 = tempBuffer;
		    if (DEBUG) System.err.println("Notifying producer");
		    bufferLock.notifyAll();
		} else {
		    if (receivedEOS)
			break;
		}
	    }
	}
	if (receivedEOS) {
	    if (DEBUG) System.err.println("Sending EOS: streamingEnabled is " + streamingEnabled);
	    // Close the file and flag it
	    if (raFile != null) {
		if (!streamingEnabled) {
		    try {
			raFile.close();
		    } catch (IOException ioe) {
		    }
		    raFile = null;
		}
		fileClosed = true;
	    }
	    if (!streamingEnabled) {
		sendEndofStreamEvent();
	    }
	}
	if (errorEncountered && state != CLOSED) {
	    close(errorReason);
	}
    }

    public synchronized long seek(long where) {
	nextLocation = where;
	return where;
    }

    long lastSyncTime = -1;

    private void write(byte [] buffer, long location, int length) {
	int offset, toWrite;
	try {
	    if (location != -1)
		doSeek(location);
	    offset = 0;
	    while (length > 0) {
		toWrite = WRITE_CHUNK_SIZE;
		if (length < toWrite)
		    toWrite = length;
		raFile.write(buffer, offset, toWrite);
		bytesWritten += toWrite;

		// Sync/Flush after a few write so that the
		// file writing is smooth. Improves capture smoothness

		/*
		if (fileDescriptor != null) {
		    // Sync'ing the file system at every 1 sec interval.
		    if (lastSyncTime < 0)
			lastSyncTime = System.currentTimeMillis();
		    else {
			long ts = System.currentTimeMillis();
			if (ts - lastSyncTime > 1000L) {
			    fileDescriptor.sync();
			    //System.err.println("sync: byte written: " + bytesWritten);
			    bytesWritten = 0;
			    lastSyncTime = ts;
			}
		    }
		}
		*/
		
		if (  fileDescriptor != null && syncEnabled &&
		      bytesWritten >= WRITE_CHUNK_SIZE) {
		    bytesWritten -= WRITE_CHUNK_SIZE;
		    fileDescriptor.sync();
		}


		filePointer += toWrite;
		length -= toWrite;
		offset += toWrite;
		if (filePointer > fileSize)
		    fileSize = filePointer;
		Thread.yield();
	    }
	} catch (IOException ioe) {
	    errorEncountered = true;
	    errorReason = ioe.toString();
	}
    }

    public long doSeek(long where) {
	if (raFile != null) {
	    try {
		raFile.seek(where);
		filePointer = (int) where;
		return where;
	    } catch (IOException ioe) {
		close("Error in seek: " + ioe);
	    }
	}
	return -1;
    }

    public long tell() {
	return nextLocation;
    }

    public long doTell() {
	if (raFile != null) {
	    try {
		return raFile.getFilePointer();
	    } catch (IOException ioe) {
		close("Error in tell: " + ioe);
	    }
	}
	return -1;
    }

    public boolean isRandomAccess() {
	return true;
    }

    private boolean deleteFile(File file) {

	boolean fileDeleted=false;
	try {
	    if ( /*securityPrivelege &&*/ (jmfSecurity != null) ) {
		try {
		    if (jmfSecurity.getName().startsWith("jmf-security")) {
			jmfSecurity.requestPermission(m, cl, args, JMFSecurity.DELETE_FILE);
			m[0].invoke(cl[0], args[0]);
		    } else if (jmfSecurity.getName().startsWith("internet")) {
			PolicyEngine.checkPermission(PermissionID.FILEIO);
			PolicyEngine.assertPermission(PermissionID.FILEIO);
		    }
		} catch (Exception e) {
		    if (JMFSecurityManager.DEBUG) {
			System.err.println("Unable to get DELETE_FILE " +
					   " privilege  " + e);
		    }
		    securityPrivelege = false;
		    // TODO: Do the right thing if permissions cannot be obtained.
		    // User should be notified via an event, if applicable
		}
	    }
 	    if ( (jmfSecurity != null) && (jmfSecurity.getName().startsWith("jdk12"))) {
		Constructor cons = jdk12DeleteFileAction.cons;
		Boolean success;
		success = (Boolean) jdk12.doPrivM.invoke(
					 jdk12.ac,
					 new Object[] {
			        cons.newInstance(
					 new Object[] {
				           file
                                         })
				});
		fileDeleted = success.booleanValue();
	    } else {
		fileDeleted = file.delete();
	    }
	} catch (Throwable e) {
	}
        return fileDeleted;
    }

}

